// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.unit;

import bats.Assets;
import bats.Bats;
import bats.SpatialProperties;
import bats.control.BatsDelayControl;
import bats.control.DeleteControl;
import bats.control.physics.BatsBubbleControl;
import bats.scene.SceneNode;
import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.Bone;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MySkeleton;
import jme3utilities.MySpatial;
import jme3utilities.MyString;
import jme3utilities.math.MyMath;
import jme3utilities.math.VectorXZ;

/**
 * All the "bob" units in a particular scene.
 * <p>
 * A "bob" is an unarmored humanoid unit with Caucasian skin, short hair, and
 * pointy ears. Three models are available: bob, izi, and nan. Each consists of
 * one geometry with 606 triangles.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class Bobs
        extends UnitClass {
    // *************************************************************************
    // constants

    /**
     * hardness setting (in meters per second, &ge;0)
     */
    final public static float hardness = 0.1f;
    /**
     * when a bob is impaired, it will switch to bubble mode after this time
     * interval (in seconds)
     */
    final private static float impairTimeout = 5f;
    /**
     * maximum ground clearance of a standard-sized bob (in meters)
     */
    final public static float standardMaxClearance = 0.3f;
    /**
     * expected number of geometries in the model
     */
    final private static int geometriesInModel = 1;
    /**
     * message logger for this class
     */
    final private static Logger logger = Logger.getLogger(Bobs.class.getName());
    /**
     * pickType for bobs
     */
    final private static String pickType = "bob";
    /**
     * approximate model coordinates for a bob's base at initial load
     */
    final public static Vector3f baseOffset = new Vector3f(0f, -0.87f, 0f);
    /**
     * offset for a bob's pick indicator relative to its base (in model units)
     */
    final private static Vector3f indicatorOffset = new Vector3f(0f, 0.3f, 0f);
    // *************************************************************************
    // constructors

    /**
     * Instantiate for the specified scene.
     *
     * @param scene where these bobs reside (not null)
     */
    public Bobs(SceneNode scene) {
        super(scene, pickType);

        setStandardMass(75f); // kilograms
        setStandardScale(1f);
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Create a collision shape for a bob.
     *
     * @param model which bob (not null)
     * @return a new instance
     */
    CollisionShape createShape(Spatial model) {
        CompoundCollisionShape result = new CompoundCollisionShape();

        BobControl bobControl = model.getControl(BobControl.class);
        if (bobControl == null) {
            return result;
        }
        /*
         * If the control had been added, create a torso shape.
         */
        bobControl.addTorsoShape(result);
        if (bobControl.isImpaired()) {
            return result;
        }
        /*
         * If the bob is unimpaired, add its solid bones to the shape.
         * (Once the bob becomes impaired, these bones become separate
         * objects joined by joints.)
         */
        Vector3f torsoCenter = getTorsoCenter();
        Map<String, SolidBone> bones = bobControl.getSolidBones();
        for (String name : bones.keySet()) {
            SolidBone bone = bones.get(name);
            bone.addTo(result, torsoCenter);
        }

        return result;
    }

    /**
     * Calculate the maximum ground clearance of a specified bob.
     *
     * @param spatial which bob to measure (not null)
     * @return ground clearance (in meters)
     */
    public float getMaxClearance(Spatial spatial) {
        assert spatial != null;

        float relativeSize = getRelativeSize(spatial);
        float clearance = relativeSize * standardMaxClearance;

        return clearance;
    }

    /**
     * Get the model coordinates of the center of a bob's torso.
     *
     * @return a new vector
     */
    static Vector3f getTorsoCenter() {
        Vector3f result = new Vector3f(0f, 0f, 0f);
        return result;
    }
    // *************************************************************************
    // UnitClass methods

    /**
     * Create a bob and add it to the scene and the pick, with its base at the
     * specified world coordinates.
     *
     * @param relativeSize the unit's size relative to the standard (&gt;0)
     * @param baseLocation world coordinates of the unit's base (not null)
     * @param direction initial direction to face (unit vector)
     * @return a new parented node to represent the unit
     */
    @Override
    public Node add(float relativeSize, String modelName, Vector3f baseLocation,
            VectorXZ direction) {
        assert relativeSize > 0f : relativeSize;
        assert modelName != null;
        assert baseLocation != null;
        assert direction != null;
        assert direction.isUnitVector() : direction;
        /*
         * Load the model, which consists of a single geometry.
         */
        String modelPath =
                String.format("Models/units/%s/%s.j3o", modelName, modelName);
        Node node = Assets.loadTexturedModel(modelPath, geometriesInModel);
        /*
         * Set game properties, name, and shadow mode.
         */
        SpatialProperties.setHardness(node, hardness);
        SpatialProperties.setPickType(node, pickType);
        node.setName(uniqueName());
        node.setShadowMode(ShadowMode.Cast);
        /*
         * Detach the geometry from the main node because it hasn't
         * been revealed yet.
         */
        Geometry geometry = (Geometry) node.getChild(0);
        node.detachChild(geometry);
        /*
         * Get the unit's diffuse texture and apply it to the geometry.
         */
        String texturePath = getTexturePath(modelName);
        Material material = Assets.createMaterial(texturePath);
        geometry.setMaterial(material);
        /*
         * Attach the main node to the world.
         */
        scene.getWorld().attach(node);
        /*
         * Set the scale factor.
         */
        float scaleFactor = scene.getScaleFactor();
        float modelScale = standardScale * relativeSize / scaleFactor;
        node.setLocalScale(modelScale);
        assert getRelativeSize(node) == relativeSize : relativeSize;
        /*
         * Initialize the unit's orientation.
         */
        float yRotateAngle = FastMath.HALF_PI - direction.azimuth();
        node.rotate(0f, yRotateAngle, 0f);
        /*
         * Initialize the unit's location.
         */
        Vector3f temp = baseOffset.mult(-modelScale);
        Vector3f centerInWorld = baseLocation.add(temp);
        MySpatial.setWorldLocation(node, centerInWorld);
        /*
         * Add controls for various behaviors.
         */
        BobControl bobControl = new BobControl(node, scene, direction);
        node.addControl(bobControl);

        SignalControl signalControl = new SignalControl(this);
        node.addControl(signalControl);

        addAbstractControls(geometry, bobControl, node);

        if (Bats.isSimulationMode()) {
            /*
             * Initialize the skeleton to balanced-neutral pose.
             */
            AnimControl animControl = node.getControl(AnimControl.class);
            AnimChannel whole = animControl.createChannel();
            whole.setAnim("balanced-neutral");
        }
        /*
         * The viewpoint is controlled by rotating the "neck" bone.
         */
        Bone neck = MySkeleton.getBone(node, "neck");
        neck.setUserControl(true);
        resetViewpoint(node);

        assert !MySpatial.isOrphan(node);
        return node;
    }

    /**
     * Get the world coordinates of a bob's pick indicator.
     *
     * @param spatial which bob to locate (not null)
     * @return a new vector
     */
    @Override
    public Vector3f getIndicatorLocation(Spatial spatial) {
        BobControl bobControl = spatial.getControl(BobControl.class);
        Vector3f local = bobControl.getBase();
        local.addLocal(indicatorOffset);
        Vector3f result = spatial.localToWorld(local, null);
        return result;
    }

    /**
     * Get the model direction which a bob's pelvis faces.
     *
     * @param spatial which bob to orient (not null)
     * @return a new unit vector in model space
     */
    @Override
    protected Vector3f getModelPelvicDirection(Spatial spatial) {
        assert spatial != null;

        return Vector3f.UNIT_Z.clone(); // TODO consider the armature?
    }

    /**
     * Get the world coordinates of a bob's target(s).
     *
     * @param spatial which bob to locate (not null)
     * @return a new list of new vectors
     */
    @Override
    public Collection<Vector3f> getTargets(Spatial spatial) {
        assert spatial != null;

        ArrayList<Vector3f> result = new ArrayList<>();
        /*
         * just one target for now: the bob's heart
         */
        Vector3f location = MySkeleton.worldLocation(spatial, "heart");
        result.add(location);

        return result;
    }

    /**
     * Get the world coordinates of a bob's viewpoint.
     *
     * @param spatial which bob to locate (not null)
     * @return a new vector
     */
    @Override
    public Vector3f getViewpointLocation(Spatial spatial) {
        assert spatial != null;

        Vector3f location = MySkeleton.worldLocation(spatial, "face");
        return location;
    }

    /**
     * Get the world orientation of a bob's viewpoint.
     *
     * @param spatial which bob (not null)
     * @return a new instance
     */
    @Override
    public Quaternion getViewpointOrientation(Spatial spatial) {
        assert spatial != null;

        Quaternion orientation = MySkeleton.worldOrientation(spatial, "face");
        /*
         * Convert the bone orientation to a camera orientation.
         */
        Quaternion conversionFactor =
                new Quaternion(-1f, 0f, 0f, 1f).normalizeLocal();
        Quaternion result = orientation.mult(conversionFactor);
        return result;
    }

    /**
     * Impair a bob.
     *
     * @param spatial which bob to impair (not null)
     */
    @Override
    public void impair(Spatial spatial) {
        assert spatial != null;

        if (isImpaired(spatial)) {
            /*
             * Once the bob is impaired, this method no longer has any effect.
             */
            return;
        }
        if (SpatialProperties.isHidden(spatial)) {
            /*
             * As long as the bob is hidden, this method has no effect.
             */
            return;
        }
        logger.log(Level.INFO, "impairing {0}", spatial.getName());

        BobControl bobControl = spatial.getControl(BobControl.class);
        bobControl.impair();
        /*
         * Un-pick the unit and make it unpickable.
         */
        SpatialProperties.setPicked(spatial, false);
        SpatialProperties.clearPickType(spatial);
        scene.getWorld().attach(spatial);

        AnimControl animControl = spatial.getControl(AnimControl.class);
        animControl.setEnabled(false);
        SignalControl signalControl = spatial.getControl(SignalControl.class);
        signalControl.setEnabled(false);
        /*
         * The bob converts to a bubble after it's been impaired for some time.
         */
        DeleteControl deleteControl = spatial.getControl(DeleteControl.class);

        CollisionShape shape = bobControl.getCollisionShape();
        PhysicsSpace space = scene.getWorld().getPhysics().getSpace();
        BatsBubbleControl bubble =
                new BatsBubbleControl(shape, space, deleteControl);
        spatial.addControl(bubble);

        BatsDelayControl delayControl = new BatsDelayControl();
        spatial.addControl(delayControl);
        delayControl.addSlave(bubble);
        delayControl.setRemainingSeconds(impairTimeout);
        delayControl.setEnabled(true);
    }

    /**
     * Test whether the specified bob is impaired.
     *
     * @param spatial which bob to test (not null)
     * @return true if the bob is impaired; false if not a bob or not impaired
     */
    @Override
    public boolean isImpaired(Spatial spatial) {
        BobControl bobControl = spatial.getControl(BobControl.class);
        if (bobControl == null) {
            return false;
        }
        boolean result = bobControl.isImpaired();
        return result;
    }

    /**
     * Reset a bob's viewpoint to its default position.
     *
     * @param spatial which bob to adjust (not null)
     */
    @Override
    public void resetViewpoint(Spatial spatial) {
        assert spatial != null;

        super.resetViewpoint(spatial);
        updateNeck(spatial);
    }

    /**
     * Tilt a bob's viewpoint forward or back by a specified amount.
     *
     * @param spatial which bob to adjust (not null)
     * @param angle in radians, positive to tilt forward, negative to tilt back
     */
    @Override
    public void tiltViewpointForward(Spatial spatial, float angle) {
        assert spatial != null;

        float tiltAngle = SpatialProperties.getTiltAngle(spatial);
        float maxTilt = 1f; // radians
        tiltAngle = MyMath.clamp(tiltAngle + angle, maxTilt);
        SpatialProperties.setTiltAngle(spatial, tiltAngle);

        updateNeck(spatial);
    }

    /**
     * Turn a bob's viewpoint left or right by a specified amount.
     *
     * @param spatial which bob to adjust (not null)
     * @param angle in radians, positive to turn left, negative to turn right
     */
    @Override
    public void turnViewpointLeft(Spatial spatial, float angle) {
        assert spatial != null;

        float turnAngle = SpatialProperties.getTurnAngle(spatial);
        float maxTurn = 1f; // radians
        turnAngle = MyMath.clamp(turnAngle + angle, maxTurn);
        SpatialProperties.setTurnAngle(spatial, turnAngle);

        updateNeck(spatial);
    }

    /**
     * Update a bob's collision shape.
     *
     * @param spatial which bob to update (not null)
     */
    @Override
    public void updateCollisionShape(Spatial spatial) {
        BobControl bobControl = spatial.getControl(BobControl.class);
        bobControl.updateCollisionShape();
    }
    // *************************************************************************
    // private methods

    /**
     * Look up the asset path to the texture of the named model.
     *
     * @param modelName name of model ("bob" or "nan")
     */
    private String getTexturePath(String modelName) {
        assert modelName != null;

        switch (modelName) {
            case "bob":
                return "Textures/units/bob/bob-blend.jpg";
            case "nan":
                return "Textures/units/nan/nan-bc.jpg";
        }
        logger.log(Level.SEVERE, "Unknown model {0}",
                MyString.quote(modelName));
        return "";
    }

    /**
     * Rotate a bob's neck to reflect the latest viewpoint angles.
     *
     * @param spatial which bob to update (not null)
     */
    private void updateNeck(Spatial spatial) {
        assert spatial != null;

        float tiltAngle = SpatialProperties.getTiltAngle(spatial);
        float turnAngle = SpatialProperties.getTurnAngle(spatial);
        Quaternion neckRotation = new Quaternion();
        neckRotation.fromAngles(-tiltAngle, turnAngle, 0.25f * turnAngle);
        Bone neck = MySkeleton.getBone(spatial, "neck");
        neck.setUserTransforms(Vector3f.ZERO, neckRotation, Vector3f.UNIT_XYZ);
    }
}